/*
 * Copyright the original author or authors.
 * 
 * Licensed under the MOZILLA PUBLIC LICENSE, Version 1.1 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.mozilla.org/MPL/MPL-1.1.html
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 package com.blitscreen.gesture
{
	import flash.display.DisplayObjectContainer;
	import flash.events.MouseEvent;
	
	import com.blitscreen.events.FastEventBroadcaster;
	import com.blitscreen.gesture.strategy.GestureHitMode;
	import com.blitscreen.render.BlitSprite;	

	/**
	 * The <code>GestureManager</code> provides interaction between the
	 * mouse and the the sprites of any <code>DrawPass</code> object. 
	 * As the output of a <code>DrawPass</code> is a <code>BitmapData</code>
	 * object there is no interactivity with objects within the bitmap. 
	 * The <code>GestureManager</code> object emulates the classical 
	 * behaviors that <code>DisplayObject</code> supports.
	 * <p>
	 * The gesture manager needs a gesture provider object, more formally 
	 * the <code>DisplayObjectContainer</code> object thatcontains the screen.
	 * The manager will register to its mouse events if there's at least
	 * one listener on it.
	 * </p><p>
	 * Hit detection is performed using a <code>GestureHitMode</code> object.
	 * By default the hit strategy is a <code>BoundingBoxHit</code> instance. In
	 * that mode, collision occurs when the mouse enters in the bouding box of
	 * the sprite.
	 * </p><p>
	 * As a screen, and objects on it, can be affected by scrolling, the mouse 
	 * coordinates needs to be offset in order to solve collisions properly.
	 * You can specify the offset to apply to the gesture mabager by using the
	 * <code>setScreenOffset</code> function.
	 * </p><p>
	 * You not register as listener for a sprite but directly for the manager,
	 * the sprite that is currently interacting with the mouse is passed within
	 * the event object. As there is no recursivity in a bitmap based display
	 * list there's no need for a bubbling event system.
	 * </p><p>
	 * The <code>GestureManager</code> class dispach the following events : 
	 * <ul>
	 * <li><strong>onPress :</strong> When the user press the mouse button over a sprite.</li>	 * <li><strong>onRelease :</strong> When the user release the mouse over a sprite.</li>	 * <li><strong>onReleaseOutside :</strong> When the user release the mouse outside of
	 * a previously pressed sprite.</li>	 * <li><strong>onRollOver :</strong> When the mouse enters in a sprite according to the
	 * current <code>GestureHitMode</code> object.</li>	 * <li><strong>onRollOut :</strong> When the mouse leaves the sprite above which it was, 
	 * according to the current <code>GestureHitMode</code> object.</li>
	 * <li><strong>onDragOut :</strong> When the mouse leave a sprite which have been
	 * previously pressed and not released yet.</li>	 * <li><strong>onDragOver :</strong> When the mouse enter a sprite which have previously 
	 * trigger an <code>onDragOut</code> event.</li>
	 * </ul>
	 * </p>
	 * @author	Cédric Néhémie
	 * @see		GestureHitModes
	 * @see		com.blitscreen.gesture.strategy.GestureHitMode
	 * @see		com.blitscreen.gesture.strategy.BoundingBoxHit
	 */
	public class GestureManager
	{
		/**
		 * Event broadcaster for this <code>GestureManager</code>.
		 */
		protected var _oEB : FastEventBroadcaster;
		/**
		 * Collision detection strategy for this <code>GestureManager</code>.
		 */
		protected var _oGestureHitMode : GestureHitMode;
		
		/**
		 * Object that contains the screen and which will provide
		 * mouse events for this gesture manager.
		 */
		protected var _doGestureProvider : DisplayObjectContainer;		
		/**
		 * Array of sprites which will interact with the mouse.
		 */
		protected var _aSprites : Array;
		
		/**
		 * A reference to the last sprite which was under the mouse.
		 * If there is no sprite under the mouse this property stores
		 * <code>null</code>.
		 */
		protected var _oSpriteUnderMouse : BlitSprite;
		/**
		 * A reference to the last sprite which was under the mouse
		 * when the mouse button was pressed. If there is no sprite
		 * under the mouse, or if the mouse button isn't pressed
		 * this property stores <code>null</code>.
		 */
		protected var _oSpriteUnderMouseWhenPressed : BlitSprite;
		/**
		 * A <code>Boolean</code> which indicates if the mouse
		 * have been pressed over a sprite or not.
		 */
		protected var _bPressedOverASprite : Boolean;
		/**
		 * A <code>Boolean</code> which indicates if the mouse
		 * have been pressed over a sprite and then moved outside
		 * of the sprite on which it was pressed.
		 */
		protected var _bIsDragging : Boolean;
		
		/**
		 * The value of offset for the X axis.
		 */
		protected var _nXOffset : Number;
		/**
		 *  The value of offset for the Y axis.
		 */
		protected var _nYOffset : Number;
		
		/**
		 * Creates a new <code>GestureManager</code> object for the passed-in
		 * array of sprites, using the passed-in gesture provider object and
		 * the optionnal <code>GestureHitMode</code> object.
		 * <p>
		 * The sprites array is stored as reference, in that way there's no
		 * need to manually update the manager in order to allow it to use
		 * newly added sprite.
		 * </p><p>
		 * If there's no <code>GestureHitMode</code> object specify at
		 * creation, the default value is used, which is a <code>BoundingBoxHit</code>
		 * object.
		 * </p>
		 * @param sprites			<code>Array</code> of sprites which will 
		 * 							interact with the mouse
		 * @param gestureProvider	<code>DisplayObjectContainer</code> object
		 * 							which will provide the basic mouse control
		 * 							for this manager, the gesture provider is
		 * 							generally the container of the screen
		 * @param hitMode			(optional) <code>GestureHitMode</code> object
		 * 							which defines the rules for the collision
		 * 							detection between sprites and the mouse. If 
		 * 							not specified, a <code>BoundingBoxHit</code>
		 * 							object is used instead
		 * @see   GestureHitModes
		 * @see	  GestureHitMode
		 */
		public function GestureManager ( sprites : Array, gestureProvider : DisplayObjectContainer, hitMode : GestureHitMode = null )
		{
			_doGestureProvider = gestureProvider;
			_aSprites = sprites;
			_oGestureHitMode = hitMode ? hitMode : GestureHitModes.BOUNDING_BOX_MODE;
			
			_oEB = new FastEventBroadcaster( this, GestureListener );
			_oSpriteUnderMouse = null;
			_oSpriteUnderMouseWhenPressed = null;
			_bPressedOverASprite = false;
			_bIsDragging = false;
			_nXOffset = 0;			_nYOffset = 0;
		}

		/**
		 * Defines the strategy used to detect collisions between
		 * the sprites and the mouse.
		 * 
		 * @param mode <code>GestureHitMode</code> object to use
		 * 			   for collision detection. If mode is 
		 * 			   <code>null</code> the mode isn't modified
		 * @see   GestureHitModes
		 */
		public function setGestureHitMode ( mode : GestureHitMode ) : void
		{
			_oGestureHitMode = mode ? mode : _oGestureHitMode;
		}		
		/**
		 * Returns the current strategy used for collision detection
		 * between the sprites and the mouse.
		 * 
		 * @return <code>GestureHitMode</code> object used for
		 * 		   collision detection. The value will never be
		 * 		   <code>null</code> 
		 * @see    GestureHitModes
		 */
		public function getGestureHitMode () : GestureHitMode
		{
			return _oGestureHitMode;
		}
		
		/**
		 * Defines the offset to apply to mouse coordinates when 
		 * computing collisions between the sprites and the mouse.
		 * <p>
		 * Screen offset is a way to handle scrolling of screen 
		 * elements without breaking interactivity.
		 * </p>
		 * @param x	<code>Number</code> value of offset on the X axis
		 * @param y <code>Number</code> value of offset on the Y axis
		 */
		public function setScreenOffset ( x : Number, y : Number ) : void
		{
			_nXOffset = x;
			_nYOffset = y;
		}		
		/**
		 * Returns the sprite which is currently under the mouse.
		 * If there is no sprite under the mouse, the function 
		 * return <code>null</code>.
		 * 
		 * @return <code>BlitSprite</code> under the mouse, or
		 * 		   <code>null</code>
		 * @see    com.blitscreen.render.BlitSprite
		 */
		public function getSpriteUnderMouse () : BlitSprite
		{
			return _oSpriteUnderMouse;
		}		
		/**
		 * Returns the sprite which was under the mouse at the
		 * last pressed time. The function returns <code>null</code>
		 * if the mouse button is not pressed or if the mouse have
		 * been pressed in an empty area of the screen.
		 * 
		 * @return <code>BlitSprite</code> under the mouse when mouse
		 * 		   button is pressed, or <code>null</code> in all
		 * 		   other cases
		 */
		public function getSpriteUnderMouseWhenPressed () : BlitSprite
		{
			return _oSpriteUnderMouseWhenPressed;
		}
		
		/**
		 * Handles the <code>MOUSE_MOVE</code> event from
		 * the gesture provider object.
		 * 
		 * @param e	<code>MouseEvent</code> object dispathed with the call
		 */	
		public function mouseMoved ( e : MouseEvent ) : void 
		{
			var l : Number = _aSprites.length;
			var x : Number = e.localX + _nXOffset;
			var y : Number = e.localY + _nYOffset;
			
			while( l-- )
			{
				var s : BlitSprite = _aSprites[l] as BlitSprite;
				
				// A sprite is under the mouse
				if( _oGestureHitMode.isTouched( x, y, s ) )
				{
					// Mouse is already over that sprite since last frame
					if( s == _oSpriteUnderMouse )
					{
						return;						
					}
					// The sprite under the mouse is not the same that
					// the last one, rollout on the first and rollover
					// on the new one
					else if( _oSpriteUnderMouse != null )
					{
						fireOnRollOutEvent( _oSpriteUnderMouse );
						_oSpriteUnderMouse = s;
						fireOnRollOverEvent( _oSpriteUnderMouse );
						return;
					} 
					// When dragging, the sprite under mouse is null
					// but the sprite under the mouse when pressed is not
					else if( _bIsDragging )
					{ 
						// The sprite under the mouse when pressed is 
						// the same that the last one it's a drag over
						if( _oSpriteUnderMouseWhenPressed == s )
						{
							_oSpriteUnderMouse = s;
							fireOnDragOverEvent( _oSpriteUnderMouse );
							return;
						}
						// The sprite under the mouse when pressed is not
						// the same that the last one, no way
						else
						{
							return;
						}
					}
					// Mouse is over a new sprite, it's a rollover
					else
					{
						_oSpriteUnderMouse = s;
						fireOnRollOverEvent( _oSpriteUnderMouse );
						return;	
					}
				}
			}
			// Mouse is over no sprite and mouse 
			// have been pressed before, it's drag out 
			if( _oSpriteUnderMouseWhenPressed )
			{
				fireOnDragOutEvent( _oSpriteUnderMouseWhenPressed );
				_oSpriteUnderMouse = null;
				return;
			}
			// Mouse is over no sprite and if one is stored anyway,
			// the mouse has gone away that sprite
			else if( _oSpriteUnderMouse )
			{
				fireOnRollOutEvent( _oSpriteUnderMouse );
				_oSpriteUnderMouse = null;
				return;
			}
		}		
		/**
		 * Handles the <code>MOUSE_DOWN</code> event from
		 * the gesture provider object.
		 * 
		 * @param e	<code>MouseEvent</code> object dispathed with the call
		 */		
		public function mousePressed ( e : MouseEvent ) : void 
		{
			// If there's a sprite under the mouse, we are pressing it
			if( _oSpriteUnderMouse != null )
			{
				_oSpriteUnderMouseWhenPressed = _oSpriteUnderMouse;
				fireOnPressEvent( _oSpriteUnderMouse );
			}
		}		
		/**
		 * Handles the <code>MOUSE_UP</code> event from
		 * the gesture provider object.
		 * 
		 * @param e	<code>MouseEvent</code> object dispathed with the call
		 */		
		public function mouseReleased ( e : MouseEvent ) : void 
		{
			// If there's a sprite under the mouse
			if( _oSpriteUnderMouse != null )
			{
				// If that sprite is the same than when we pressed mouse,
				// it's a release
				if( _oSpriteUnderMouseWhenPressed == _oSpriteUnderMouse )
				{
					fireOnReleaseEvent( _oSpriteUnderMouseWhenPressed );
					_oSpriteUnderMouseWhenPressed = null;
				}
				// If not, it's a release outside
				else
				{
					fireOnReleaseOutsideEvent( _oSpriteUnderMouseWhenPressed );
					_oSpriteUnderMouseWhenPressed = null;
				}				
			}
			// If there's no sprite under the mouse but a sprite when pressed,
			// it's a release outside
			else if( _oSpriteUnderMouseWhenPressed )
			{
				fireOnReleaseOutsideEvent( _oSpriteUnderMouseWhenPressed );
				_oSpriteUnderMouseWhenPressed = null;
			}
		}
		
		/**
		 * Adds the passed-in object as listener for the current
		 * <code>GestureManager</code> object.
		 * <p>
		 * If the passed-in object is the first listener for this
		 * gesture manager, the gesture manager will register to the
		 * gesture provider events.
		 * </p>
		 * @param listener <code>GestureListener</code> to add as listener
		 * 				   for this gesture manager
		 */
		public function addGestureListener ( listener : GestureListener ) : void
		{
			if( _oEB.isEmpty() )
			{
				_doGestureProvider.addEventListener( MouseEvent.MOUSE_MOVE, mouseMoved );
				_doGestureProvider.addEventListener( MouseEvent.MOUSE_DOWN, mousePressed );
				_doGestureProvider.addEventListener( MouseEvent.MOUSE_UP, mouseReleased );
			}
			
			_oEB.addListener( listener );
		}		
		/**
		 * Removes the passed-in object from listening to the current
		 * gesture manager.
		 * <p>
		 * If the passed-in object is the last listener for this
		 * gesture manager, the gesture manager will unregister
		 * to the gesture provider events.
		 * </p>
		 * @param listener <code>GestureListener</code> to remove from listening
		 * 				   to this gesture manager
		 */
		public function removeGestureListener ( listener : GestureListener ) : void
		{
			_oEB.removeListener( listener );
			
			if( _oEB.isEmpty() )
			{
				_doGestureProvider.removeEventListener( MouseEvent.MOUSE_MOVE, mouseMoved );
				_doGestureProvider.removeEventListener( MouseEvent.MOUSE_DOWN, mousePressed );
				_doGestureProvider.removeEventListener( MouseEvent.MOUSE_UP, mouseReleased );
			}
		}
		/**
		 * Fires an <code>onPress</code> event for the passed-in 
		 * <code>BlitSprite</code> object. 
		 * 
		 * @param sprite <code>BlitSprite</code> object which have been pressed
		 * @see	  GestureEvent
		 */
		protected function fireOnPressEvent ( sprite : BlitSprite ) : void 
		{
			var event : GestureEvent = new GestureEvent( GestureEvent.onPressEVENT, sprite, this );
			_oEB.broadcastEvent( event );
		}
		/**
		 * Fires an <code>onRelease</code> event for the passed-in 
		 * <code>BlitSprite</code> object. 
		 * 
		 * @param sprite <code>BlitSprite</code> object which have been released
		 * @see	  GestureEvent
		 */
		protected function fireOnReleaseEvent ( sprite : BlitSprite ) : void 
		{
			var event : GestureEvent = new GestureEvent( GestureEvent.onReleaseEVENT, sprite, this );
			_oEB.broadcastEvent( event );
		}
		/**
		 * Fires an <code>onReleaseOutside</code> event for the passed-in 
		 * <code>BlitSprite</code> object. 
		 * 
		 * @param sprite <code>BlitSprite</code> object which have been
		 * 				 released outside
		 * @see	  GestureEvent
		 */
		protected function fireOnReleaseOutsideEvent ( sprite : BlitSprite ) : void 
		{
			var event : GestureEvent = new GestureEvent( GestureEvent.onReleaseOutsideEVENT, sprite, this );
			_oEB.broadcastEvent( event );
		}
		/**
		 * Fires an <code>onRollOver</code> event for the passed-in 
		 * <code>BlitSprite</code> object. 
		 * 
		 * @param sprite <code>BlitSprite</code> object which is under the mouse
		 * @see	  GestureEvent
		 */
		protected function fireOnRollOverEvent ( sprite : BlitSprite ) : void 
		{			
			var event : GestureEvent = new GestureEvent( GestureEvent.onRollOverEVENT, sprite, this );
			_oEB.broadcastEvent( event );
		}
		/**
		 * Fires an <code>onRollOut</code> event for the passed-in 
		 * <code>BlitSprite</code> object. 
		 * 
		 * @param sprite <code>BlitSprite</code> object which was under the mouse
		 * @see	  GestureEvent
		 */
		protected function fireOnRollOutEvent ( sprite : BlitSprite ) : void 
		{
			var event : GestureEvent = new GestureEvent( GestureEvent.onRollOutEVENT, sprite, this );
			_oEB.broadcastEvent( event );
		}
		/**
		 * Fires an <code>onDragOver</code> event for the passed-in 
		 * <code>BlitSprite</code> object. 
		 * 
		 * @param sprite <code>BlitSprite</code> object which is under the mouse
		 * 				 when mouse was pressed
		 * @see	  GestureEvent
		 */
		protected function fireOnDragOverEvent ( sprite : BlitSprite ) : void 
		{
			var event : GestureEvent = new GestureEvent( GestureEvent.onDragOverEVENT, sprite, this );
			_oEB.broadcastEvent( event );
		}
		/**
		 * Fires an <code>onDragOut</code> event for the passed-in 
		 * <code>BlitSprite</code> object. 
		 * 
		 * @param sprite <code>BlitSprite</code> object which was under the mouse
		 * 				 when mouse was pressed
		 * @see	  GestureEvent
		 */
		protected function fireOnDragOutEvent ( sprite : BlitSprite ) : void 
		{
			var event : GestureEvent = new GestureEvent( GestureEvent.onDragOutEVENT, sprite, this );
			_oEB.broadcastEvent( event );
		}		
	}
}